define(['./mbDomUtils', './mbElevationUtils'], function (mbDomUtils, mbElevationUtils) {

    var $mm = {};

    function noop() {
    }

//======================================================================================================================
//region Image Toggle Markers
//======================================================================================================================

    // noinspection JSUnusedLocalSymbols
    function MarkerLabelOffset(opts) {
        // will be translated to $(size)px
        this.iconSize = 0;
        // will be translated to $(pct)%
        this.labelSize = 0;
        var self = this;
        Object.keys(opts || {}).forEach(function (k) {
            self[k] = opts[k];
        });
    }

    var availableMarkerLabelAnchorTransforms = {
        top: {
            // translate(0, -100%)
            translateY: {
                labelSize: -1,
            }
        },
        right: {
            // translate(32px, 0)
            translateX: {
                iconSize: 1,
            }
        },
        bottom: {
            // translate(0, 32px)
            translateY: {
                iconSize: 1,
            }
        },
        left: {
            // translate(-100%, 0)
            translateX: {
                labelSize: -1,
            }
        },
        center: {
            translateX: {
                labelSize: -0.5,
                iconSize: 0.5,
            },
            translateY: {
                labelSize: -0.5,
                iconSize: 0.5,
            }
        },
    };
    var defaultMarkerLabelAnchor = 'right';

    function resolveMarkerLabelAnchorOption(opts) {
        if (!opts) return;
        var anchor = opts ? opts.anchor : defaultMarkerLabelAnchor;
        if (!availableMarkerLabelAnchorTransforms[anchor]) {
            anchor = defaultMarkerLabelAnchor;
        }
        return anchor;
    }

    var DEFAULT_PIXEL = {x: 0, y: 0};

    function resolveMarkerLabelOffsetOption(opts) {
        if (!opts) return;
        return opts.offset || DEFAULT_PIXEL;
    }

    var digitMultiplier = {}

    function formatFraction(p, maxDigit, unit) {
        if (maxDigit === undefined) {
            maxDigit = 2;
        }
        var multiplier = digitMultiplier[maxDigit];
        if (!multiplier) {
            multiplier = digitMultiplier[maxDigit] = Math.pow(10, maxDigit);
        }
        p = (~~(p * multiplier)) / multiplier;
        if (unit) {
            return p + unit;
        }
        return p;
    }

    function setMarkerLabelAnchor(labelElem, anchor, iconSize, offset, sync) {
        var transform = availableMarkerLabelAnchorTransforms[anchor];
        if (!transform || !labelElem) return;

        var keysToCheck = {translateX: 'translateX', translateY: 'translateY'};

        var clauses = [];
        Object.keys(keysToCheck).forEach(function (key) {
            var config = transform[key];
            if (!config) return;
            if (config.labelSize) {
                clauses.push(key + '(' + formatFraction(config.labelSize * 100, 2, '%') + ')')
            }
            if (config.iconSize) {
                clauses.push(key + '(' + formatFraction(config.iconSize * iconSize, 2, 'px') + ')');
            }
        });

        if (offset) {
            if (offset.x) {
                clauses.push('translateX(' + formatFraction(offset.x, 2, 'px') + ')');
            }
            if (offset.y) {
                clauses.push('translateY(' + formatFraction(offset.y, 2, 'px') + ')');
            }
        }

        mbDomUtils.requestDomUpdate(function () {
            labelElem.style.transform = clauses.join(' ');
        }, sync);
    }

    function setMarkerLabelShown(labelElem, shown, sync) {
        if (shown) {
            mbDomUtils.addClassToElem(labelElem, 'label_shown', sync);
            mbDomUtils.setElemVisible(labelElem, true);
        } else {
            mbDomUtils.removeClassFromElem(labelElem, 'label_shown', sync);
            mbDomUtils.setElemVisible(labelElem, false);
        }
    }

    /*
     * TODO: reuse <img> element
     */

    function createEmptyImageMarkerDiv(icon, label, iconSize, zIndex) {

        var div = document.createElement('div');
        div.setAttribute('class', 'mbmap_marker mbmap_toggle_marker');

        /*
         * Inline CSS
         */
        div.style.position = 'absolute';
        div.style.left = '0';
        div.style.top = '0';

        return div;
    }

    function createImageMarkerDiv(icon, label, iconSize, zIndex) {

        iconSize = iconSize || 0;

        var div = createEmptyImageMarkerDiv(icon, label, iconSize, zIndex);

        fillImageMarkerContent(div, icon, label, iconSize, zIndex);

        return div;
    }

    function fillImageMarkerContent(div, icon, label, iconSize, zIndex) {

        var zIndexStr = str(zIndex !== undefined ? zIndex : markerNormalZIndex);

        var text = label ? (label.content || label.text) : undefined;
        var labelStatus = text !== undefined ? 'label_shown' : '';

        var anchor = resolveMarkerLabelAnchorOption(label);
        var offset = resolveMarkerLabelOffsetOption(label);

        var customClass = '' + (label ? (label.customClass || '') : '');

        // updates will be done in next frame (rAF)
        mbDomUtils.requestDomUpdate(function () {

            // TODO: do DOM creation async in workers
            {
                var markerContent = document.createElement('div');
                markerContent.setAttribute('class', 'mbmap_marker_content');
                markerContent.style.position = 'absolute';
                markerContent.style.width = '100%';
                markerContent.style.height = '100%';

                {
                    var img = document.createElement('img');
                    img.setAttribute('class', 'mbmap_marker_img');
                    if (icon) {
                        img.src = icon;
                    }
                    img.style.width = '100%';
                    img.style.height = '100%';
                    img.style.maxWidth = '100%';
                    img.style.maxHeight = '100%';
                    markerContent.appendChild(img);

                    var markerLabel = document.createElement('div');
                    markerLabel.setAttribute('class', 'mbmap_marker_label slim ' + labelStatus + ' ' + anchor + ' ' + customClass);
                    markerContent.appendChild(markerLabel);

                    {
                        var labelContent = document.createElement('span');
                        labelContent.setAttribute('class', 'mbmap_marker_label_content');
                        labelContent.innerText = (text !== undefined ? label.content : '');
                        markerLabel.appendChild(labelContent);
                    }
                }

                /*
                 * This will be executed async, when others added some content into it, this will be messed up
                 */
                div.appendChild(markerContent);
            }

            var labelElem = div.querySelector('.mbmap_marker_label');
            setMarkerLabelAnchor(labelElem, anchor, iconSize, offset, true);
            setMarkerLabelShown(labelElem, text !== undefined, true);

            div.style.zIndex = zIndexStr;
        });

    }

    function wrapImageMarkerLabel(content) {
        if (!(content instanceof HTMLElement)) {
            // wrap with a <span> element
            var span = document.createElement('span');
            span.setAttribute('class', 'mbmap_marker_label_content');
            span.innerHTML = content;
            content = span;
            // displaying text
        } else {
            // displaying custom DOM element
        }
        return content;
    }

    var markerTopZIndex = 20;
    var markerNormalZIndex = 10;
    var markerNotTopZIndex = 10;

    function str(v) {
        if (typeof v === 'string') return v;
        return v + '';
    }

    /*
     * Options
     * offset: PixelLike
     * normalImage: string
     * selectedImage: string
     * iconSize: number
     * visible: boolean
     * showLabelOnHover: boolean
     * label: MBMarkerLabelOptions
     */
    // currently only rectangle supported (w=h)
    $mm.addOutdoorMarker = function (lng, lat, opts) {

        if (!window.mapboxgl) return;

        opts = opts || {};

        var normalImage = opts.normalImage;
        var selectedImage = opts.selectedImage;

        //assuming selected and normal icons of same size
        var iconSize = opts.iconSize;

        var selectedIcon = selectedImage;
        var icon = normalImage;

        var markerOpts = {
            anchor: opts.anchor || 'center',
            offset: DEFAULT_PIXEL,
            draggable: false,
            clickable: true,
            rotation: 0,
            pitchAlignment: 'viewport',
            rotationAlignment: 'viewport',
            visible: opts.visible !== undefined ? opts.visible : true,
        };

        Object.keys(opts).forEach(function (key) {
            markerOpts[key] = opts[key];
        });

        if (opts.offset !== undefined) {
            markerOpts.offset = xyToPointLike(opts.offset);
        }

        if (!opts.showLabelOnHover) {
            markerOpts.label = opts.label;
        }

        if (opts.draggable !== undefined) {
            markerOpts.draggable = opts.draggable;
        }
        if (opts.raiseOnDrag !== undefined) {
            // TODO: to be impl
            markerOpts.raiseOnDrag = opts.raiseOnDrag;
        }

        var element = createEmptyImageMarkerDiv(normalImage, markerOpts.label, iconSize);
        element.style.width = iconSize + 'px';
        element.style.height = iconSize + 'px';
        element.style.zIndex = str(markerNormalZIndex);
        markerOpts.element = element;

        element.addEventListener('mousemove', function (e) {
            marker.fire('mousemove', e);
        });

        var marker = new mapboxgl.Marker(markerOpts);
        if (!isNaN(lng)) {
            marker.setLngLat([lng, lat]);
        }

        marker._onceCallbacks = {};
        marker.callOnce = function (ev, callback) {
            if (!marker._onceCallbacks[ev]) {
                marker._onceCallbacks[ev] = [];
            }
            marker._onceCallbacks[ev].push(callback);
        };

        marker.fireOnce = function (ev) {
            var callbacks = marker._onceCallbacks[ev] || [];
            delete marker._onceCallbacks[ev];
            var args = [].concat(arguments);
            args.splice(0, 1);
            callbacks.forEach(function (cb) {
                cb.apply(this, args);
            });
        };

        marker.opts = opts;

        marker.visible = markerOpts.visible === undefined || markerOpts.visible;

        // associate the DOM object to the marker object, NOTE might this cause memory leak?
        marker._element._mbmarkerObject = marker;

        var doCreateMarker = function () {
            if (!opts.skipInitialMarker) {
                fillImageMarkerContent(element, normalImage, markerOpts.label, iconSize);
            }
        };

        if (markerOpts.visible) {
            doCreateMarker();
        } else {
            marker.callOnce('onShow', doCreateMarker);
        }

        /*
         * Set html content of marker (not label)
         */
        marker.setContent = function (html) {

            opts.skipInitialMarker = true;

            var elem = html;
            if (typeof html === 'string') {
                var p = document.createElement('div');
                p.innerHTML = html;
                elem = p.children[0];
            }

            // the order of css operations will be messed up if some parts are not executed in fastdom while some are
            mbDomUtils.requestDomUpdate(function () {
                var oldElem = marker._element;

                while (oldElem.firstChild) {
                    oldElem.removeChild(oldElem.firstChild);
                }

                oldElem.appendChild(elem);
            });
        };

        // TODO: marker onclick
        var clicked = opts.clicked || noop;

        marker.on('click', function (eventName, obj, obj2, obj3, obj4) {

            if (!marker.visible) return;
            if (!markerOpts.clickable) return;

            if (clicked) {
                clicked(this);
            }
        });

        marker.oldIcon = icon;
        marker.selectedIcon = selectedIcon;

        marker.setIcon = function (icon) {

            var iconSize = opts.iconSize;

            // AMap.Icon compatability
            if (typeof icon === 'object') {
                if (icon && icon.get) {
                    var imageSize = icon.get('imageSize');
                    if (imageSize) {
                        opts.iconSize = imageSize.width || iconSize;
                    }
                    icon = icon.get('image') || icon;
                }
            }

            mbDomUtils.requestDomUpdate(function () {
                var img = marker._element.querySelector('.mbmap_marker_img');
                if (img) {
                    img.setAttribute('src', icon || null);
                }
            });
        };

        /*
         * new option added:
         * options.label.anchor in `availableMarkerLabelAnchorTransforms`
         */
        marker.setLabel = function (opts) {
            var doShow = !!opts;
            opts = opts || {};
            var anchor = resolveMarkerLabelAnchorOption(opts);
            var offset = resolveMarkerLabelOffsetOption(opts);

            mbDomUtils.requestDomUpdate(function () {
                var labelElem = marker._element.querySelector('.mbmap_marker_label');

                setMarkerLabelAnchor(labelElem, anchor, iconSize, offset, true);

                var content = wrapImageMarkerLabel(opts.content);
                mbDomUtils.setInnerHtml(labelElem, content, true);

                setMarkerLabelShown(labelElem, doShow, true);

                if (opts.customClass) {
                    mbDomUtils.addClassToElem(labelElem, opts.customClass);
                }
            });
        };

        // sets the style of label text
        marker.setStyle = function (style) {
            var labelElem = marker._element.querySelector('.mbmap_marker_label');
            if (labelElem) {
                mbDomUtils.requestDomUpdate(function () {
                    Object.keys(style).forEach(function (key) {
                        labelElem.style[key] = style[key];
                    });
                });
            }
        };

        marker.addLabelClass = function (clz) {
            var labelElem = marker._element.querySelector('.mbmap_marker_label');
            if (labelElem) {
                mbDomUtils.addClassToElem(labelElem, clz);
            }
        };

        marker.removeLabelClass = function (clz) {
            var labelElem = marker._element.querySelector('.mbmap_marker_label');
            if (labelElem) {
                mbDomUtils.removeClassFromElem(labelElem, clz);
            }
        };

        marker.setTop = function (isTop) {
            mbDomUtils.requestDomUpdate(function () {
                if (marker._element) {
                    marker._element.style.zIndex = str(isTop ? markerTopZIndex : markerNotTopZIndex);
                }
            });
        };

        marker.setSelected = function (selected) {
            if (selected === undefined || selected) {
                if (this.selectedIcon) {
                    this.setIcon(this.selectedIcon);
                }
                this.setTop(true);
            } else {
                if (this.oldIcon) {
                    this.setIcon(this.oldIcon);
                }
            }
            if (marker.visible) {
                marker.show();
            } else {
                marker.hide();
            }
        };

        var oldSetLngLat = marker.setLngLat;
        marker.setLngLat = function (lng, lat) {
            if (typeof lng === 'number' && typeof lat === 'number') {
                oldSetLngLat.call(marker, [lng, lat]);
            } else {
                oldSetLngLat.apply(marker, arguments);
            }
        };

        marker.setPosition = marker.setLngLat;

        var earthRadius = 6371008.8;
        var earthCircumfrence = 2 * Math.PI * earthRadius;

        marker.loadElevation = function () {
            var showingElevation = opts.isShowingElevation;
            if (showingElevation && !showingElevation()) {
                return;
            }
            var self = this;
            if (opts.elevation === undefined && !self._lastRequestElevation && self.visible) {
                self._lastRequestElevation = Date.now();
                var loc = marker.getPosition();
                if (!loc) return;

                var lng = loc.lng;
                var lat = loc.lat;
                mbElevationUtils.altitudeTileSet.readElevationDataAtGcj(lng, lat, opts.altitudeZoom || mbElevationUtils.altitudeQueryZoom, function (elev) {
                    self.opts.elevation = elev;
                    marker.applyElevation(elev);
                });
            }
        };

        marker.applyElevation = function (elevation, force) {
            return;
            var lngLat = this.getPosition();
            if (!this.currMap) return;

            var dx = 0;
            var dy = 0;

            if (elevation) {

                elevation = elevation - (this.currMap.baseElevation || 0);

                var transform = this.currMap.transform;

                var zoom = this.currMap.getZoom();
                // TODO: magic number
                var ratio = Math.pow(2, zoom - 15.9);

                // var screen = transform.locationPoint(lngLat, elevation / ratio);
                var screen = mbElevationUtils.transform_locationPoint(transform, lngLat, elevation / ratio);
                var screenFoot = transform.locationPoint(lngLat);

                dy = screen.y - screenFoot.y;
                dx = screen.x - screenFoot.x;
            }

            if (marker._dx === dx && marker._dy === dy) {
                return;
            }

            // TODO: mapbox.setBearing is constantly calling this for all markers, reduce performance impact of this

            marker._dx = dx;
            marker._dy = dy;
            marker._lastSetElevation = elevation;

            var updateStyle = function () {
                mbDomUtils.requestDomUpdate(function () {
                    marker._element.style.left = marker._dx + 'px';
                    marker._element.style.top = marker._dy + 'px';
                });
            };

            // if it's not visible, postpone to first time setting to visible
            if (marker.visible) {
                updateStyle();
            } else {
                marker.callOnce('onShow', function () {
                    updateStyle();
                });
            }
        };

        marker.refreshElevation = function () {
            var showingElevation = opts.isShowingElevation;
            if (showingElevation && !showingElevation()) {
                marker.applyElevation(0);
                return;
            }
            if (opts.elevation !== undefined) {
                marker.applyElevation(opts.elevation);
            } else {
                marker.loadElevation();
            }
        };

        marker.setLabelContent = function (opts) {
            var labelElem = marker._element.querySelector('.mbmap_marker_label');
            if (!labelElem) return;
            if (!opts) {
                mbDomUtils.removeClassFromElem(labelElem, 'label_shown');
                mbDomUtils.setElemVisible(labelElem, false);
                return;
            }

            if (typeof opts === 'string') {
                opts = {
                    content: opts,
                };
            }

            this.setLabel({
                offset: [opts.offsetX, opts.offsetY],
                content: opts.text || opts.content,
            });
        };

        marker.setText = function (text) {
            mbDomUtils.requestDomUpdate(function () {
                marker.setLabelContent({
                    content: text,
                    text: text,
                });
            });
        };

        marker._mapUpdated = function (map) {
            if (map && !map.getCanvasContainer) {
                // not a valid mbmap
                return;
            }
            this.currMap = map;

            marker.on('move', marker.refreshElevation);
            marker.on('moveend', marker.refreshElevation);
            marker.on('terrainToggle', marker.refreshElevation);

            this.refreshElevation();
        }

        marker.removeFromMap = function (scope) {
            this._mapUpdated(null);
            this.remove();

            if (marker.currMap) {
                marker.currMap.off('move', marker.refreshElevation);
                marker.currMap.off('moveend', marker.refreshElevation);
                marker.currMap.off('terrainToggle', marker.refreshElevation);
            }

        };

        marker.addToMap = function (scope) {
            // map without addSource might not be a valid mapbox map instance
            if (!scope || !scope.map || !scope.map.addSource) return;
            this._mapUpdated(scope.map);
            this.addTo(scope.map);

            if (scope && scope.map) {
                scope.map.on('move', marker.refreshElevation);
                scope.map.on('moveend', marker.refreshElevation);
                scope.map.on('terrainToggle', marker.refreshElevation);
            }
        };

        marker.setMap = function (map) {
            this._mapUpdated(map);
            // map without addSource might not be a valid mapbox map instance
            if (map && map.addSource) {
                marker.addToMap({map: map});
            } else {
                marker.removeFromMap({map: map})
            }
        };

        marker.getPosition = function () {
            var lnglat = marker.getLngLat();
            lnglat.getLng = function () {
                return this.lng;
            };
            lnglat.getLat = function () {
                return this.lat;
            };
            return lnglat;
        }

        marker.hide = function () {
            mbDomUtils.addClassToElem(marker._element, 'mbmap_marker_hidden');
            mbDomUtils.setElemVisible(marker._element, false);
            marker.setDraggable(false);
            marker.visible = false;
            marker.fire('onHide');
            marker.fireOnce('onHide');
        };

        marker.show = function () {
            mbDomUtils.removeClassFromElem(marker._element, 'mbmap_marker_hidden');
            mbDomUtils.setElemVisible(marker._element, true);
            marker.setDraggable(markerOpts.draggable);
            marker.visible = true;
            marker.fire('onShow');
            marker.fireOnce('onShow');
        };

        marker.setClickable = function (c) {
            markerOpts.clickable = c;
        };

        marker.getClickable = function () {
            return markerOpts.clickable;
        };

        return marker;
    };

    $mm.addOutdoorText = function (lng, lat, opts) {
        var marker = $mm.addOutdoorToggleMarker(lng, lat, opts);
        marker.addLabelClass('slim');
        return marker;
    };

//======================================================================================================================
//endregion
//======================================================================================================================

    function xyToPointLike(p) {
        var x = 0;
        var y = 0;
        if (p instanceof Array) {
            x = p[0];
            y = p[1];
        } else if (typeof p === 'object') {
            if (p && p.getX) {
                x = p.getX();
            } else {
                x = p.x || 0;
            }
            if (p && p.getY) {
                y = p.getY();
            } else {
                y = p.y || 0;
            }
        }
        return [x, y];
    }

    return $mm;

});
